/*---------------------------------------------------------------
*  Copyright 2005 by the Radiological Society of North America
*
*  This source software is released under the terms of the
*  RSNA Public License (http://mirc.rsna.org/rsnapubliclicense)
*----------------------------------------------------------------*/

/*---------------------------------------------------------------
*  Modified to allow pixel data to be skipped, and released under
*  New BSD licensing terms.
*----------------------------------------------------------------*/

package com.novartis.nims.trialsubmitter.dicom;

import java.io.BufferedInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Properties;

import org.dcm4che.data.Dataset;
import org.dcm4che.data.DcmDecodeParam;
import org.dcm4che.data.DcmEncodeParam;
import org.dcm4che.data.DcmParser;
import org.dcm4che.data.DcmElement;
import org.dcm4che.data.FileFormat;
import org.dcm4che.data.FileMetaInfo;
import org.dcm4che.dict.Tags;
import org.dcm4che.dict.UIDs;
import org.dcm4che.data.DcmObjectFactory;
import org.dcm4che.data.DcmParserFactory;
import org.dcm4che.dict.DictionaryFactory;
import org.dcm4che.dict.TagDictionary;
import org.dcm4che.dict.VRs;

import org.rsna.ctp.stdstages.anonymizer.AnonymizerFunctions;
import org.rsna.ctp.stdstages.anonymizer.AnonymizerStatus;
import org.rsna.ctp.stdstages.anonymizer.dicom.FnCall;

import org.apache.log4j.Logger;

/**
 * The MIRC DICOM anonymizer. The anonymizer provides de-identification and
 * re-identification of DICOM objects for clinical trials. Each element
 * as well as certain groups of elements are scriptable. The script
 * language is defined in "How to Configure the Anonymizer for MIRC
 * Clinical Trial Services".
 * <p>
 * See the <a href="http://mirc.rsna.org/mircdocumentation">
 * MIRC documentation</a> for more more information.
 */
public class DicomCleaner {

	static final Logger logger = Logger.getLogger(DicomCleaner.class);
	static final DcmParserFactory pFact = DcmParserFactory.getInstance();
	static final DcmObjectFactory oFact = DcmObjectFactory.getInstance();
	static final DictionaryFactory dFact = DictionaryFactory.getInstance();
	static final TagDictionary tagDictionary = dFact.getDefaultTagDictionary();

   /**
     * Anonymizes the input file, writing the result to the output file.
     * The input and output files are allowed to be the same.
     * The fields to anonymize are scripted in the properties file.
     * The Remapper instance provides a link either to a local or remote
     * remapper for PHI replacement where the remapped elements must maintain
     * their relationships with other remapped elements.
     * <p>
     * Important note: if the script generates a skip() or quarantine()
     * function call, the output file is not written and the input file
     * is unmodified, even if it is the same as the output file.
     * @param inFile the file to anonymize.
     * @param outFile the output file.  It may be same as inFile you if want
     *      to anonymize in place.
     * @param cmds the properties file containing the anonymization commands.
     * @param lkup the properties file containing the local lookup table; null if local
     * lookup is not to be used.
     * @param forceIVRLE force the transfer syntax to IVRLE if true; leave
     * the syntax unmodified if false.
     * @param renameToSOPIUID rename the output file to [SOPInstanceUID].dcm, where
     * [SOPInstanceUID] is the value in the anonymized object (in case it is
     * remapped during anonymization.
     * @param passThroughPixelData force the rest of the file starting with the pixel
     * data to be passed through to the output as a byte stream. WARNING! this could
     * cause an invalid DICOM to be output, and should only be used when the pixel data
     * are already corrupt.
     * remapped during anonymization.
     * @return the list of exceptions
     */
    public static AnonymizerStatus anonymize(
			File inFile,
			File outFile,
			Properties cmds,
			Properties lkup,
			boolean forceIVRLE,
			boolean renameToSOPIUID,
			boolean passThroughPixelData) {
		String exceptions = "";
		BufferedInputStream in = null;
		FileOutputStream out = null;
		File tempFile = null;
		byte[] buffer = new byte[4096];
		try {
			//Check that this is a known format.
			in = new BufferedInputStream(new FileInputStream(inFile));
			DcmParser parser = pFact.newDcmParser(in);
			FileFormat fileFormat = parser.detectFileFormat();
			if (fileFormat == null) throw new IOException("Unrecognized file format: "+inFile);

			//Get the dataset (excluding pixels) and leave the input stream open
			Dataset dataset = oFact.newDataset();
			parser.setDcmHandler(dataset.getDcmHandler());
			parser.parseDcmFile(fileFormat, Tags.PixelData);
			
			
			RuleParser ruleParser = new RuleParser(cmds);
			Properties nestedCommands = ruleParser.selectNestedElementRules();
			logger.info("DicomAnonymizer processing file "+inFile);
			processCommands(0, dataset, cmds, nestedCommands);

			//Save the dataset.
			//Write to a temporary file in a temporary directory
			//on the same file system root, and rename at the end.
			File tempDir = new File(outFile.getParentFile().getParentFile(),"anonymizer-temp");
			tempDir.mkdirs();
			tempFile = File.createTempFile("DCMtemp-",".anon",tempDir);
            out = new FileOutputStream(tempFile);

            //Get the SOPInstanceUID in case we need it for the rename.
            String sopiUID = null;
			try { sopiUID = dataset.getString(Tags.SOPInstanceUID); }
			catch (Exception e) { };
			sopiUID = sopiUID.trim();

			//Set the encoding
			DcmDecodeParam fileParam = parser.getDcmDecodeParam();
        	String prefEncodingUID = UIDs.ImplicitVRLittleEndian;
			FileMetaInfo fmi = dataset.getFileMetaInfo();
            if ((fmi != null) && (fileParam.encapsulated || !forceIVRLE))
            	prefEncodingUID = fmi.getTransferSyntaxUID();
			DcmEncodeParam encoding = (DcmEncodeParam)DcmDecodeParam.valueOf(prefEncodingUID);
			boolean swap = fileParam.byteOrder != encoding.byteOrder;

            //Create and write the metainfo for the encoding we are using
			fmi = oFact.newFileMetaInfo(dataset, prefEncodingUID);
            dataset.setFileMetaInfo(fmi);
            fmi.write(out);

			//Write the dataset as far as was parsed
			dataset.writeDataset(out, encoding);
			
			//Write pixels and any later elements
			if (! passThroughPixelData) {
				
				//Write the pixels if the parser actually stopped before pixeldata
				if (parser.getReadTag() == Tags.PixelData) {
					dataset.writeHeader(
							out,
							encoding,
							parser.getReadTag(),
							parser.getReadVR(),
							parser.getReadLength());
					if (encoding.encapsulated) {
						parser.parseHeader();
						while (parser.getReadTag() == Tags.Item) {
							dataset.writeHeader(
									out,
									encoding,
									parser.getReadTag(),
									parser.getReadVR(),
									parser.getReadLength());
							writeValueTo(parser, buffer, out, false);
							parser.parseHeader();
						}
						if (parser.getReadTag() != Tags.SeqDelimitationItem) {
							throw new Exception(
									"Unexpected Tag: " + Tags.toString(parser.getReadTag()));
						}
						if (parser.getReadLength() != 0) {
							throw new Exception(
									"(fffe,e0dd), Length:" + parser.getReadLength());
						}
						dataset.writeHeader(
								out,
								encoding,
								Tags.SeqDelimitationItem,
								VRs.NONE,
								0);
					} else {
						writeValueTo(parser, buffer, out, swap && (parser.getReadVR() == VRs.OW));
					}
					parser.parseHeader(); //get ready for the next element
				}
				
				//Now do any elements after the pixels one at a time.
				//This is done to allow streaming of large raw data elements
				//that occur above Tags.PixelData.
				while (!parser.hasSeenEOF() && parser.getReadTag() != -1) {
					dataset.writeHeader(
							out,
							encoding,
							parser.getReadTag(),
							parser.getReadVR(),
							parser.getReadLength());
					writeValueTo(parser, buffer, out, swap);
					parser.parseHeader();
				}
			} else { //if we want to simply pass through pixel data and later elements
				
				//Write the pixels if the parser actually stopped before pixeldata
				if (parser.getReadTag() == Tags.PixelData) {
					dataset.writeHeader(
							out,
							encoding,
							parser.getReadTag(),
							parser.getReadVR(),
							parser.getReadLength());

					
					//pass through all remaining data.
					int byteValue = 0;
					while ((byteValue = in.read()) != -1) {
						out.write(byteValue);
						System.out.println(byteValue);
					}
				}
			}
			
			out.flush();
			out.close();
			in.close();
			if (renameToSOPIUID) outFile = new File(outFile.getParentFile(),sopiUID+".dcm");
			outFile.delete();
			tempFile.renameTo(outFile);
		}

		catch (Exception e) {
			try {
				//Close the input stream if it actually got opened.
				if (in != null) in.close();
			}
			catch (Exception ignore) { }
			try {
				//Close the output stream if it actually got opened,
				//and delete the tempFile in case it is still there.
				if (out != null) {
					out.close();
					tempFile.delete();
				}
			}
			catch (Exception ignore) { }
			//Now figure out what kind of response to return.
			String msg = e.getMessage();
			if (msg == null) {
				msg = "!error! - no message";
				logger.info("Error call from "+inFile);
				return AnonymizerStatus.QUARANTINE(inFile,msg);
			}
			if (msg.indexOf("!skip!") != -1) {
				return AnonymizerStatus.SKIP(inFile,msg);
			}
			if (msg.indexOf("!quarantine!") != -1) {
				logger.info("Quarantine call from "+inFile);
				return AnonymizerStatus.QUARANTINE(inFile,msg);
			}
			logger.info("Unknown exception from "+inFile+"\nMessage: "+msg);
			return AnonymizerStatus.QUARANTINE(inFile,msg);
		}
		return AnonymizerStatus.OK(outFile,exceptions);
    }
    
    @SuppressWarnings("unchecked")
	private static void processCommands(int recursionLevel, Dataset dataset, Properties commands, Properties nestedCommands) throws Exception {
		Properties theReplacements = DicomCleaner.setUpReplacements(dataset, commands, null);
		
		//get booleans to handle the global cases
		boolean rpg = (commands.getProperty("remove.privategroups") != null);
		boolean rue = (commands.getProperty("remove.unspecifiedelements") != null);
		boolean rol = (commands.getProperty("remove.overlays") != null);
		int[] keepGroups = getKeepGroups(commands);

		//Modify the elements according to the commands
		String exceptions = DicomCleaner.doOverwrite(dataset, theReplacements, rpg, rue, rol, keepGroups);
		if (!exceptions.equals("")) {
			logger.error("DicomAnonymizer exceptions\n"+exceptions);
		}
		
		Properties commandsToApplyRecursively = commands;
		if (recursionLevel == 0) {
			RuleParser ruleParser = new RuleParser(commands);
			commandsToApplyRecursively = ruleParser.selectContentRemovalRules();
		}
		
		for (Iterator it = dataset.iterator(); it.hasNext(); ) {
			DcmElement element = (DcmElement) it.next();
			
			if (element.hasItems()) {
				for (int i = 0; i < element.countItems(); i++) {
					Dataset item = element.getItem(i);
					processCommands(recursionLevel + 1, item, commandsToApplyRecursively, nestedCommands);
				}
			}
		}
		
		processNestedCommands(dataset, nestedCommands);
	}
    
    private static void processNestedCommands(Dataset dataset, Properties commands) throws Exception {
    	Properties currentCommands = new Properties();
    	HashMap<String, Properties> childCommandsMap = new HashMap<String, Properties>();
    	
    	for (Enumeration<Object> it=commands.keys(); it.hasMoreElements(); ) {
			String key = (String) it.nextElement();
			if (key.startsWith("set.")) {
				String tagString = getTagString(key);
				
				if (tagString.length() == 11) {
					currentCommands.setProperty(key, commands.getProperty(key));
				} else if (tagString.length() > 11) {
					String tagRoot = tagString.substring(0, 5) + "," + tagString.substring(6, 10) + ")";
					String tagChild = tagString.substring(11, tagString.length() - 1);
					
					String childKey = "set.[" + tagChild + key.substring(key.indexOf("]"));
					Properties childCommands = childCommandsMap.get(tagRoot);
					if (childCommands == null) {
						childCommands = new Properties();
					}
					childCommands.setProperty(childKey, commands.getProperty(key));
					childCommandsMap.put(tagRoot, childCommands);
				}
			}
		}
    	
    	Properties theReplacements = DicomCleaner.setUpReplacements(dataset, currentCommands, null);
		String exceptions = DicomCleaner.doOverwrite(dataset, theReplacements, false, false, false, new int[0]);
		if (!exceptions.equals("")) {
			logger.error("DicomAnonymizer exceptions\n"+exceptions);
		}
		
		for (String key : childCommandsMap.keySet()) {
			int tag = getTagInt(key);
			DcmElement element = dataset.get(tag);
			
			if (element != null && element.hasItems()) {
				for (int i = 0; i < element.countItems(); i++) {
					Dataset item = element.getItem(i);
					processNestedCommands(item, childCommandsMap.get(key));
				}
			}
		}
    }

	private static void writeValueTo(
					DcmParser parser,
					byte[] buffer,
					OutputStream out,
					boolean swap) throws Exception {
		InputStream in = parser.getInputStream();
		int len = parser.getReadLength();
		if (swap && (len & 1) != 0) {
			throw new Exception(
				"Illegal length for swapping value bytes: " + len);
		}
		if (buffer == null) {
			if (swap) {
				int tmp;
				for (int i = 0; i < len; ++i, ++i) {
					tmp = in.read();
					out.write(in.read());
					out.write(tmp);
				}
			} else {
				for (int i = 0; i < len; ++i) {
					out.write(in.read());
				}
			}
		} else {
			byte tmp;
			int c, remain = len;
			while (remain > 0) {
				c = in.read(buffer, 0, Math.min(buffer.length, remain));
				if (c == -1) {
					throw new EOFException("EOF while reading element value");
				}
				if (swap) {
					if ((c & 1) != 0) {
						buffer[c++] = (byte) in.read();
					}
					for (int i = 0; i < c; ++i, ++i) {
						tmp = buffer[i];
						buffer[i] = buffer[i + 1];
						buffer[i + 1] = tmp;
					}
				}
				out.write(buffer, 0, c);
				remain -= c;
			}
		}
		parser.setStreamPosition(parser.getStreamPosition() + len);
	}

    //Create a Properties object that contains all the replacement values defined
    //by the scripts for each element. The value for an element is stored under
    //the key "(gggg,eeee)".
    static Properties setUpReplacements(
			Dataset ds,
			Properties cmds,
			Properties lkup) throws Exception {
		Properties props = new Properties();
		for (Enumeration<Object> it=cmds.keys(); it.hasMoreElements(); ) {
			String key = (String) it.nextElement();
			if (key.startsWith("set.")) {
				try {
					String replacement = makeReplacement(key,cmds,lkup,ds);
					props.setProperty(getTagString(key),replacement);
				}
				catch (Exception e) {
					String msg = e.getMessage();
					if (msg == null) msg = "";
					if (msg.indexOf("!skip!") != -1) throw e;
					if (msg.indexOf("!quarantine!") != -1) throw e;
					logger.error("Exception in setUpReplacements:",e);
					throw new Exception(
						"!error! during processing of:\n" + key + "=" + cmds.getProperty(key));
				}
			}
		}
		return props;
	}

	//Get an int[] containing all the keep.groupXXXX
	//elements' group numbers, sorted in order.
	static int[] getKeepGroups(Properties cmds) {
		LinkedList<String> list = new LinkedList<String>();
		for (Enumeration<Object> it=cmds.keys(); it.hasMoreElements(); ) {
			String key = (String)it.nextElement();
			if (key.startsWith("keep.group")) {
				list.add(key.substring("keep.group".length()).trim());
			}
		}
		Iterator<String> iter = list.iterator();
		int[] keepGroups = new int[list.size()];
		for (int i=0; i<keepGroups.length; i++) {
			try { keepGroups[i] = Integer.parseInt(iter.next(),16) << 16; }
			catch (Exception ex) { keepGroups[i] = 0; }
		}
		Arrays.sort(keepGroups);
		return keepGroups;
	}

	//Find "[gggg,eeee]" in a String and
	//return a tagString in the form "(gggg,eeee)".
	static final String defaultTagString = "(0000,0000)";
	private static String getTagString(String key) {
		int k = key.indexOf("[");
		if (k < 0) return defaultTagString;
		int kk = key.indexOf("]",k);
		if (kk < 0) return defaultTagString;
		return ("(" + key.substring(k+1,kk) + ")").toLowerCase();
	}

	//Find "gggg,eeee)" in a String and
	//return an int corresponding to the hex value.
	private static int getTagInt(String key) {
		int k = key.indexOf("(");
		if (k < 0) return 0;
		int kk = key.indexOf(")",k);
		if (kk < 0) return 0;
		key = key.substring(k+1,kk).replaceAll("[^0-9a-fA-F]","");
		try { return Integer.parseInt(key,16); }
		catch (Exception e) { return 0; }
	}

	static final char escapeChar 		= '\\';
	static final char functionChar 		= '@';
	static final char delimiterChar 	= '^';
//	static final String contentsFn 		= "contents";
//	static final String dateFn 			= "date";
//	static final String encryptFn 		= "encrypt";
//	static final String hashFn 			= "hash";
//	static final String hashnameFn 		= "hashname";
	static final String hashuidFn 		= "hashuid";
//	static final String hashptidFn 		= "hashptid";
	static final String ifFn 			= "if";
//	static final String incrementdateFn = "incrementdate";
//	static final String initialsFn 		= "initials";
//	static final String lookupFn		= "lookup";
//	static final String paramFn 		= "param";
//	static final String quarantineFn 	= "quarantine";
	static final String requireFn 		= "require";
//	static final String roundFn 		= "round";
//	static final String skipFn	 		= "skip";
//	static final String timeFn 			= "time";


	//Create the replacement for one element starting from a key.
	private static String makeReplacement(
			String key,
			Properties cmds,
			Properties lkup,
			Dataset ds)  throws Exception {
		String cmd = cmds.getProperty(key);
		int thisTag = getTagInt(getTagString(key));
		return makeReplacement(cmd,cmds,lkup,ds,thisTag);
	}

	//Create the replacement for one element starting from a command.
	private static String makeReplacement(
			String cmd,
			Properties cmds,
			Properties lkup,
			Dataset ds,
			int thisTag)  throws Exception {
		String out = "";
		char c;
		int i = 0;
		boolean escape = false;
		while (i < cmd.length()) {
			c = cmd.charAt(i++);
			if (escape) {
				out += c;
				escape = false;
			}
			else if (c == escapeChar) escape = true;
			else if (c == functionChar) {
				FnCall fnCall = new FnCall(cmd.substring(i),cmds,lkup,ds,thisTag);
				if (fnCall.length == -1) break;
				i += fnCall.length;
				/*if (fnCall.name.equals(contentsFn)) 		out += contents(fnCall);
				else if (fnCall.name.equals(dateFn)) 		out += date(fnCall);
				else if (fnCall.name.equals(encryptFn))		out += encrypt(fnCall);
				else if (fnCall.name.equals(hashFn))		out += hash(fnCall);
				else if (fnCall.name.equals(hashnameFn))	out += hashname(fnCall);
				else if (fnCall.name.equals(hashptidFn))	out += hashptid(fnCall);
				else*/ if (fnCall.name.equals(hashuidFn)) 	out += hashuid(fnCall);
				else if (fnCall.name.equals(ifFn))			out += iffn(fnCall);
//				else if (fnCall.name.equals(incrementdateFn)) out += incrementdate(fnCall);
//				else if (fnCall.name.equals(initialsFn)) 	out += initials(fnCall);
//				else if (fnCall.name.equals(lookupFn)) 		out += lookup(fnCall);
//				else if (fnCall.name.equals(paramFn)) 		out += param(fnCall);
//				else if (fnCall.name.equals(quarantineFn))	throw new Exception("!quarantine!");
				else if (fnCall.name.equals(requireFn))		out += require(fnCall);
//				else if (fnCall.name.equals(roundFn))		out += round(fnCall);
//				else if (fnCall.name.equals(skipFn))		throw new Exception("!skip!");
//				else if (fnCall.name.equals(timeFn)) 		out += time(fnCall);
				else out += functionChar + fnCall.getCall();
			}
			else out += c;
		}
		return out;
	}


	//Execute the if function call
	private static String iffn(FnCall fn) throws Exception {
		if (testCondition(fn))
			return makeReplacement(fn.trueCode,fn.cmds,fn.lkup,fn.ds,fn.thisTag);
		return makeReplacement(fn.falseCode,fn.cmds,fn.lkup,fn.ds,fn.thisTag);
	}

	//Determine whether a condition in an if statement is met
	private static boolean testCondition(FnCall fn) {
		if (fn.args.length < 2) return false;
		String tagName = fn.getArg(0);
		int tag = fn.getTag(tagName);
		String element = contents(fn.ds,tagName,tag);
		if (fn.args[1].equals("isblank")) {
			return (element == null) || element.trim().equals("");
		}
		else if (fn.args[1].equals("matches")) {
			if ((element == null) || (fn.args.length < 3)) return false;
			return element.matches(getArgument(fn.getArg(2)));
		}
		else if (fn.args[1].equals("exists")) {
			return fn.ds.contains(tag);
		}
		return false;
	}

	//Filter a quoted argument, removing the quotes
	private static String getArgument(String arg) {
		arg = arg.trim();
		if (arg.startsWith("\"") && arg.endsWith("\"")) {
			arg = arg.substring(1,arg.length()-1);
		}
		return arg;
	}

//	//Execute the contents function call.
//	//There are three possible calls:
//	//   @contents(ElementName)
//	//   @contents(ElementName,"regex")
//	//   @contents(ElementName,"regex","replacement")
//	private static String contents(FnCall fn) {
//		String value = contents(fn.ds,fn.args[0],fn.thisTag);
//		if (value == null) return null;
//		if (fn.args.length == 1) return value;
//		else if (fn.args.length == 2) return value.replaceAll(fn.getArg(1),"");
//		else if (fn.args.length == 3) return value.replaceAll(fn.getArg(1),fn.getArg(2));
//		return "";
//	}

	//Get the contents of a dataset element by tagName
	private static String contents(Dataset ds, String tagName, int defaultTag) {
		String value = "";
		tagName = (tagName != null) ? tagName.trim() : "";
		if (!tagName.equals("")) {
			int tag = tagName.equals("this") ? defaultTag : Tags.forName(tagName);
			try {value = ds.getString(tag);}
			catch (Exception e) { };
		}
		if (value == null) value = "";
		return value;
	}

//	//Execute the param function call
//	private static String param(FnCall fn) {
//		return getParam(fn.cmds,fn.args[0]);
//	}

	//Get the value of a parameter identified by a function call argument
	private static String getParam(FnCall fn) {
		return getParam(fn.cmds,fn.args[0]);
	}

	//Get the value of a parameter from the script
	private static String getParam(Properties cmds, String param) {
		param = param.trim();
		if (!param.equals("") && (param.charAt(0) == functionChar)) {
			param = (String)cmds.getProperty("param." + param.substring(1));
		}
		return param;
	}

	//Execute the require function. This function checks whether the
	//specified element is present. If it is, it leaves the element alone;
	//otherwise, it inserts an element with the specified value.
	private static String require(FnCall fn) {
		//Return a @keep() call if the element is present
		if (fn.ds.contains(fn.thisTag)) return "@keep()";

		//The element was not present, return a value for a new element
		//If there are no arguments, return an empty string.
		if (fn.args.length == 0) return "";

		//There are some arguments, get the element tag
		//and see if it is in the dataset.
		String value = null;
		int tag = fn.getTag(fn.args[0]);
		if (fn.ds.contains(tag)) {
			//It is, get the value
			try { value = fn.ds.getString(tag); }
			catch (Exception e) { };
		}
		else {
			//It isn't; get a default value from the arguments
			//or an empty string if there is no default.
			value = (fn.args.length > 1) ? fn.getArg(1) : "";
		}
		//Convert an all-blank value to a blank function call
		//so that the element isn't removed when the replacements
		//are processed.
		if (value.trim().equals("")) value = "@blank("+value.length()+")";
		return value;
	}

//	//Execute the lookup function. This function uses the value of an element
//	//as an index into a local unencrypted lookup table and returns the result.
//	//If the requested element is not present or the value of the element is not
//	//a key in the lookup table, throw a quarantine exception.
//	//The arguments are: ElementName, keyType
//	//where keyType is the name of a specific key, which must appear in the lkup
//	//Properties as keyType/value = replacement.
//	//Values and replacements are trimmed before use.
//	private static String lookup(FnCall fn) throws Exception {
//		//Validate the input.
//		if (fn.lkup == null) throw new Exception("!quarantine! missing lookup table.");
//		if (fn.lkup.size() == 0) throw new Exception("!quarantine! empty lookup table.");
//		String key = contents(fn.ds,fn.args[0],fn.thisTag);
//		if (key == null) throw new Exception("!quarantine! lookup key missing in DICOM object.");
//		key = key.trim();
//		key = fn.args[1] + "/" + key;
//		String value = fn.lkup.getProperty(key);
//		if (value == null) throw new Exception("!quarantine! unable to find key ("+key+") in lookup table.");
//		return value.trim();
//	}
//
//	//Execute the initials function. This function is typically used
//	//to generate the initials of a patient from the contents of the
//	//PatientName element.
//	private static String initials(FnCall fn) {
//		String s = contents(fn.ds,fn.args[0],fn.thisTag);
//		if (s == null) return "x";
//		s = s.replace(delimiterChar,' ');
//		s = s.replaceAll("\\s+"," ").trim();
//		if (s.equals("")) return "x";
//		String ss = "";
//		int i = 0;
//		do {
//			ss += s.charAt(i);
//			i = s.indexOf(" ",i) + 1;
//		} while (i != 0);
//		//Now move the last name initial to the end
//		if (ss.length() > 1) ss = ss.substring(1) + ss.charAt(0);
//		return ss.toUpperCase();
//	}
//
//	//Execute the hashname function. This function is typically used
//	//to generate a numeric value from the contents of the PatientName element.
//	private static String hashname(FnCall fn) {
//		try {
//			String string = contents(fn.ds,fn.args[0],fn.thisTag);
//
//			String lengthString = getParam(fn.cmds,fn.args[1]);
//			int length;
//			try { length = Integer.parseInt(lengthString); }
//			catch (Exception ex) { length = 4; }
//
//			int wordCount = Integer.MAX_VALUE;
//			if (fn.args.length > 2) {
//				String wordCountString = getParam(fn.cmds,fn.args[2]);
//				try { wordCount = Integer.parseInt(wordCountString); }
//				catch (Exception keepDefault) { }
//			}
//			return AnonymizerFunctions.hashName(string, length, wordCount);
//		}
//		catch (Exception ex) {
//			logger.warn("Exception in hashname"+fn.getArgs()+": "+ex.getMessage());
//			return fn.getArgs();
//		}
//	}
//
//	//Execute the round function call. This function is used to
//	//round an age field into groups of a given size.
//	private static String round(FnCall fn) {
//		//arg must contain: AgeElementName, groupSize
//		try {
//			String ageString = contents(fn.ds,fn.args[0],fn.thisTag);
//			String sizeString = getParam(fn.cmds,fn.args[1]);
//			int size = Integer.parseInt(sizeString);
//			return AnonymizerFunctions.round(ageString,size);
//		}
//		catch (Exception e) {
//			logger.warn("Exception caught in round"+fn.getArgs()+": "+e.getMessage());
//			return fn.getArgs();
//		}
//	}
//
//	//Execute the hashptid function call.
//	private static String hashptid(FnCall fn) {
//		//args must contain: siteid, elementname, and optionally maxlen
//		try {
//			if (fn.args.length < 2) return fn.getArgs();
//			String siteid = getParam(fn.cmds,fn.args[0]);
//			String ptid = contents(fn.ds,fn.args[1],fn.thisTag);
//			if (ptid == null) ptid = "null";
//			String maxlenString = "";
//			if (fn.args.length >= 3) maxlenString = getParam(fn.cmds,fn.args[2]);
//			int maxlen = Integer.MAX_VALUE;
//			try { maxlen = Integer.parseInt(maxlenString); }
//			catch (Exception ex) { maxlen = Integer.MAX_VALUE; }
//			return AnonymizerFunctions.hashPtID(siteid,ptid,maxlen);
//		}
//		catch (Exception e) {
//			logger.warn("Exception caught in hashptid"+fn.getArgs()+": "+e.getMessage());
//			return fn.getArgs();
//		}
//	}
//
//	//Execute the hash function call. This function is used
//	//to generate an MD5 hash of any element text, with the
//	//being a base-10 digit string.
//	private static String hash(FnCall fn) {
//		try {
//			String value = contents(fn.ds,fn.args[0],fn.thisTag);
//			String lenString = getParam(fn.cmds,fn.args[1]);
//			int len = Integer.MAX_VALUE;
//			if (lenString.length() != 0) {
//				try { len = Integer.parseInt(lenString); }
//				catch (Exception ex) { len = Integer.MAX_VALUE; }
//			}
//			return AnonymizerFunctions.hash(value,len);
//		}
//		catch (Exception e) {
//			logger.warn("Exception caught in hash"+fn.getArgs()+": "+e.getMessage());
//			return fn.getArgs();
//		}
//	}

//	//Execute the incrementdate function call.
//	//Get a new date by adding a constant to an date value.
//	private static String incrementdate(FnCall fn) {
//		//arg must contain: DateElementName, increment
//		//DateElementName is the name of the element containing the date to be incremented.
//		//increment specifies the number of days to add to the date (positive generates
//		//later dates; negative generates earlier dates.
//		String removeDate = "@remove()";
//		try {
//			String date = contents(fn.ds,fn.args[0],fn.thisTag);
//			String incString = getParam(fn.cmds,fn.args[1]);
//			long inc = Long.parseLong(incString);
//			inc *= 24 * 3600 * 1000;
//			return AnonymizerFunctions.incrementDate(date,inc);
//		}
//		catch (Exception e) {
//			logger.warn("Exception caught in incrementdate"+fn.getArgs()+": "+e.getMessage());
//			return removeDate;
//		}
//	}

	//Execute the hashuid function call. Generate a new uid
	//from a prefix and an old uid. The old uid is hashed and
	//appended to the prefix.
	private static String hashuid(FnCall fn) {
		String removeUID = "@remove()";
		try {
			if (fn.args.length != 2) return fn.getArgs();
			String prefix = getParam(fn);
			String uid = contents(fn.ds,fn.args[1],fn.thisTag);
			//If there is no UID in the dataset, then return @remove().
			if (uid == null) return removeUID;
			//Make sure the prefix ends in a period
			if (!prefix.endsWith(".")) prefix += ".";
			//Create the replacement UID
			return AnonymizerFunctions.hashUID(prefix,uid);
		}
		catch (Exception e) {
			logger.warn("Exception caught in hashuid"+fn.getArgs()+": "+e.getMessage());
			return fn.getArgs();
		}
	}

//	//Execute the encrypt function call. This function is used
//	//to generate an encrypted string from an element text value.
//	private static String encrypt(FnCall fn) {
//		if (fn.args.length < 2) return fn.getArgs();
//		try {
//			String value = contents(fn.ds,fn.args[0],fn.thisTag);
//			String key = getParam(fn.cmds,fn.args[1]);
//			return AnonymizerFunctions.encrypt(value,key);
//		}
//		catch (Exception e) {
//			logger.warn("Exception caught in encipher"+fn.getArgs()+": "+e.getMessage());
//			return fn.getArgs();
//		}
//	}
//
//	private static String time(FnCall fnCall) {
//		return AnonymizerFunctions.time(fnCall.getArg(0));
//	}
//
//	private static String date(FnCall fnCall) {
//		return AnonymizerFunctions.date(fnCall.getArg(0));
//	}

//********************** End of function calls ***************************

	//Remove and modify elements in the dataset.
	static String doOverwrite(
			Dataset ds,
			Properties theReplacements,
				// These global flags deal with groups of elements.
				// If anything appears in theReplacements for an element,
				// it overrides the action of the global flags.
				// A global keep flag overrides a global remove flag.
			boolean rpg,	//remove private groups
			boolean rue,	//remove unspecified elements
			boolean rol,	//remove overlay groups
			int[] keepGroups
			) throws Exception {
		String exceptions = "";
		String value;

		//If we are removing anything globally, then go through the dataset
		//and look at each element individually.
		GlobalRuleApplicator globalRuleApplicator = new GlobalRuleApplicator(ds, theReplacements);
		globalRuleApplicator.setRecursive(false);
		globalRuleApplicator.setRemoveOverlayGroups(rol);
		globalRuleApplicator.setRemovePrivateGroups(rpg);
		globalRuleApplicator.setRemoveUnspecifiedGroups(rue);
		globalRuleApplicator.setSpecifiedGroups(keepGroups);
		globalRuleApplicator.execute();

		//Now go through theReplacements and handle the instructions there
		for (Enumeration<Object> it = theReplacements.keys(); it.hasMoreElements(); ) {
			String key = (String) it.nextElement();
			int tag = getTagInt(key);
			value = (String)theReplacements.getProperty(key);
			value = (value != null) ? value.trim() : "";
			if (value.equals("") || (value.indexOf("@remove()") != -1)) {
//				try { recursiveRemove(ds, tag); }
				try { ds.remove(tag); }
				catch (Exception ignore) { }
			}
			else if (value.equals("@keep()")) ; //@keep() leaves the element in place
			else if (value.startsWith("@blank(")) {
				//@blank(n) inserts an element with n blank chars
				String blanks = "                                                       ";
				String nString = value.substring("@blank(".length());
				int paren = nString.indexOf(")");
				int n = 0;
				if (paren != -1) {
					nString = "0" + nString.substring(0,paren).replaceAll("\\D","");
					n = Integer.parseInt(nString);
				}
				if (n > blanks.length()) n = blanks.length();
				try { putXX(ds,tag,getVR(tag),blanks.substring(0,n)); }
				catch (Exception e) {
					logger.warn(key + " exception: " + e.toString());
					if (!exceptions.equals("")) exceptions += ", ";
					exceptions += key;
				}
			}
			else {
				try {
					if (value.equals("@empty()")) value = "";
					putXX(ds,tag,getVR(tag),value);
				}
				catch (Exception e) {
					logger.warn(key + " exception:\n" + e.toString()
								+ "\ntag=" + Integer.toHexString(tag)
								+ ": value= \"" + value + "\"");
					if (!exceptions.equals("")) exceptions += ", ";
					exceptions += key;
				}
			}
		}
		return exceptions;
	}

	private static int getVR(int tag) {
		TagDictionary.Entry entry = tagDictionary.lookup(tag);
		try { return VRs.valueOf(entry.vr); }
		catch (Exception ex) { return VRs.valueOf("SH"); }
	}

	//This method works around the bug in dcm4che which inserts the wrong
	//VR (SH) when storing an empty element of VR = PN. It also handles the
	//problem in older dcm4che versions which threw an exception when an
	//empty DA element was created. It forces the VR of private elements to UT.
	//It also removes all items from a sequence when the value to put is null
	//or empty. 
	private static void putXX(Dataset ds, int tag, int vr, String value) throws Exception {
		if ((value == null) || value.equals("")) {
			if (vr == VRs.PN) {
				ds.putXX(tag,vr," ");
			} else if (vr == VRs.SQ) {
				ds.remove(tag);
				ds.putSQ(tag);
			} else {
				ds.putXX(tag,vr);
			}
		}
		else if ((tag&0x10000) != 0) ds.putUT(tag,value);
		else ds.putXX(tag,vr,value);
	}
	
//	//This method removes the specified tag from the specified dataset, and also
//	//from any dataset nested inside, at any level
//	private static void recursiveRemove(Dataset ds, int tag) {
//		LinkedList<Integer> tagsToRemove = new LinkedList<Integer>();
//		
//		for (Iterator it = ds.iterator(); it.hasNext(); ) {
//			DcmElement element = (DcmElement) it.next();
//			
//			if (element.tag() == tag) {
//				tagsToRemove.add(tag);
//			} else {
//				if (element.hasItems()) {
//					for (int i = 0; i < element.countItems(); i++) {
//						Dataset item = element.getItem(i);
//						recursiveRemove(item, tag);
//					}
//				}
//			}
//		}
//		
//		for (int tagToRemove : tagsToRemove) {
//			try {
//				ds.remove(tagToRemove);
//			} catch (Exception ignore) { }
//		}
//	}
	
}
